#ifndef __tenacitas_log__logger_factory__h__
#define __tenacitas_log__logger_factory__h__


/** @file
    In this file is defined the @p tenacitas::log::logger_factory class
*/

/** @todo */


// ==> C++ Headers
#include <string>
#include <cstdint>
#include <mutex>
#include <map>
#include <utility>
#include <sstream>
#include <memory>
#include <functional>

// ==> 3rds Headers

// ==> Our Headers
/** In the <tt>level.h</tt> file we have the definition of the log levels  */
#include "level.h"
//#include "logger.h"
#include <tenacitas.exception/exception.h>

// ==> Namespaces
using namespace std;

// ==> Macro Constants
/** */

// ==> Macro Commands

/** ==> namespace of the organization */
namespace tenacitas {

    /** ==> namespace of the project */
    namespace log {

        // ==> Typedefs 

        // ==> Pre-Declarations

        // ==> Attributes 
        /** */

        // ==> Functions
        
        // ==> Classes

        /** Default factory class for logger object 
           @nosubgrouping
        */
        template <typename t_suppplier>
        class logger_factory {
        public:
            
            // ==> logger_factory - Public Internal Types

            /** */
            typedef t_supplier supplier;

            /** */
            typedef typename logger<supplier> logger;
            
            // ==> logger_factory - Public Constructors

            /** Constructor

                @param  p_level is the tenacitas::log::level to be used
                by this @p logger object. Its default value is the log level
                defined for all the @p logger objects, defined using the
                @p set_level_all below, and retrieved via @p get_level_all

                @note Please, read the documentation for tenacitas::log::level
                to understand how log levels work in tenacitas::log

            */
            logger_factory(level p_level = logger::get_level_all());
            
            
            /** Destructor */
            ~logger_factory();

            /** Creates a pointer to a logger object

                @param p_level is the level specific for this logger object. It
                assumes the default value of the log level assigned to all the
                logger objects, retrieved by @p get_level_all() method

                @return a pointer to a @p logger object, if it was possible to
                create it, or a @p nullptr value otherwise

                @throw tenacitas::exception::exception */
            logger & create(level p_level = level::info);
            
            /** Copies are not-allowed */
            logger_factory (const logger_factory & p_logger) = delete;

            /** Moves are not-allowed */
            logger_factory (logger_factory && p_logger) = delete;

            // ==> logger_factory - Public Accessors


            // ==> logger_factory - Public Processors

            // ==> logger_factory - Public Operators
            
            
        private:

            // ==> logger_factory - Private Internal Types

            /** Type for the @p t_media parameter */
            typedef t_media media;

            /** Type for the pointer to a @p t_media object */
            typedef typename media::ptr media_ptr;

            /** Responsible for creating and mantaining @p logger objects */
            class factory {

            public:

                // ==>  factory - Public Constructors

                /** Constructor */
                factory();

                /** Destructor */
                ~factory();

                /** Creates (a pointer to) a @p logger object

                    @param p_level is the tenacitas::log::level specific to this @p
                    logger object. The default value is the tenacitas::log::level
                    defined for all the @p logger objects, retrieved by the @p
                    tenacitas::log::logger::get_level_all method

                    @return a pointer to a @p logger object, if it was possible to
                    create it, or a @p nullptr value otherwise

                    @throw tenacitas::exception::exception */
                logger::ptr create(level p_level = get_level_all());


                // ==>  factory - Public Processors

                /** Retrieves a @p logger_ptr associated to a thread

                    This method will search for a @p logger_ptr in the collection of
                    @p logger_ptr, and it will return it; or it will create a
                    @p logger_ptr, associate it to the thread, and insert it into
                    the collection

                    @param p_level is the log level specific for this @p logger
                    being created
                */
                logger::ptr get_logger(level p_level);


                /** Deletes a <tt>(logger::ptr, boost::thread::id)</tt>
                    association from the collection 

                    @param  p_thread_id is the thread to be removed, together to
                    its associated @p logger_ptr, from the collection
                */
                void delete_logger(boost::thread::id p_thread_id);
                
            private:

                // ==>  factory - Private Internal Types

                /** Function object to be called when a thread ends */
                struct at_thread_end {

                    // ==> at_thread_end - Public Constructors

                    /** Constructor
                        @param p_factory is the @p logger class itself
                    */
                    at_thread_end(factory * p_factory)
                        : m_factory (p_factory){}

                    // ==> at_thread_end - Public Operators

                    /** Operator to be called when a thread ends */
                    void operator()() {
                        m_factory->delete_logger(boost::this_thread::get_id());
                    }

                private:

                    /** @p factory object responsible for deleting a
                        <tt>(logger::ptr, boost::thread::id)</tt> association */
                    factory * m_factory;
                };

                // ==>  factory - Private Attributes

                /** Type of the collection of the thread and the media where log
                    messages will be recorded */
                typedef std::map<boost::thread::id, logger::ptr> map;

                /** The type of iterator of the collection above */
                typedef typename map::iterator iterator;

                /** collection of the thread and the media where log messages
                    will be recorded */
                map m_map;

                /** To protect access to @p m_map */
                std::mutex m_mutex;
            };


            // ==> logger_factory - Private Constructor


            // ==> logger_factory - Private Accessors

            /** Retrieves the @p factory object */
            static factory & get_factory();
            
            // ==> logger_factory - Private Attributes

            /** Log level of this @p logger object */
            level m_default_level;

        };
    }
}

using namespace tenacitas::log;

// =============================================================================
//                     I M P L E M E N T A T I O N
// =============================================================================

// #############################################################################
// logger::factory

// -----------------------------------------------------------------------------
template <typename t_media_factory>
inline logger_factory<t_media>::
logger_factory(level p_level = level::info)
    : m_default_level(p_level) {}

      
    

// -----------------------------------------------------------------------------
template <typename t_media_factory>
inline logger_factory<t_media>::factory::
factory()
    : m_map(),
      m_mutex(){}


// -----------------------------------------------------------------------------
template <typename t_media_factory>
logger_factory<t_media>::factory::
~factory(){
    try {
        // first we protect access to the map
        std::lock_guard<std::mutex> lock(m_mutex);

        // for every (logger, thread) association...
        iterator l_end = m_map.end();
        for (iterator l_ite = m_map.begin();
              l_ite != l_end;
              ++l_ite) {

            // save a reference to the media
            logger::ptr l_logger = l_ite->second;

            // erase the association from the map
            m_map.erase(l_ite);

        }
    }
    catch (tenacitas::exception::exception & l_ex) {
        std::cerr << l_ex.what() << std::endl;
    }
    catch (std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;
    }
}

// -----------------------------------------------------------------------------
template <typename t_media_factory>
inline typename logger_factory<t_media>::ptr
logger_factory<t_media>::factory::
create(level p_level) {
    return get_logger(p_level);
}


// -----------------------------------------------------------------------------
template <typename t_media_factory>
typename logger_factory<t_media>::ptr
logger_factory<t_media>::factory::
get_logger(level p_level) {

    boost::thread::id l_thread_id = boost::this_thread::get_id(); 
    try {

        // already created?
        iterator l_ite = m_map.find(l_thread_id); 

        if (l_ite != m_map.end()) {
            // yes!
            return (l_ite->second);
        }

        // inserting the logger into the collection of loggers,
        // associating to theirs threads
        logger::ptr l_logger (new logger(this, p_level));

        std::pair<boost::thread::id, logger::ptr> l_pair(l_thread_id, l_logger); 

        // I can lock only here, because the thread l_th_id can not be
        // modified in another thread
        std::lock_guard<std::mutex> lock(m_mutex);

        // defining the callback to when the thread ends
        boost::this_thread::at_thread_exit(at_thread_end(this));

        // returning a reference to the just inserted logger
        return (m_map.insert(l_pair).first->second);
    }
    catch (tenacitas::exception::exception & l_ex) {
        std::cerr << l_ex.what() << std::endl;
        throw l_ex;
    }
    catch (std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;
        throw l_tenacitas_ex;
    }
}

// -----------------------------------------------------------------------------
template <typename t_media_factory>
void
logger_factory<t_media>::factory::
delete_logger(boost::thread::id p_thread_id) {

    try {
        iterator l_ite = m_map.find(p_thread_id);

        if (l_ite != m_map.end()) {

            logger::ptr l_logger = l_ite->second;

            {
                // I can lock only here, because exclusion of p_thread_id can
                // not happen in another thread
                std::lock_guard<std::mutex> lock(m_mutex);

                m_map.erase(l_ite);
            }

        }
    }
    catch (tenacitas::exception::exception & l_ex) {
        std::cerr << l_ex.what() << std::endl;
        throw l_ex;
    }
    catch (std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;
        throw l_tenacitas_ex;
    }
}

#endif
